import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Stack;

import javax.swing.BorderFactory;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class MapPanel extends JPanel implements MouseListener, MouseMotionListener, Serializable
{
    private static final long serialVersionUID = 5793346957479184521L;

    private final int RIGHT_MOUSE_MODIFIER = 4;

    private Map displayMap;
    private ArrayList<Map> mapList;
    private SystemState state;
    private MapObject lastObjectCreated;
    private JSlider slider;
    private Stack<MapMemory> undoStack;
    private Stack<MapMemory> redoStack;
    private Dimension startingDimension;
    private HelpHandler helpHandler;

    public MapPanel(JSlider slider)
    {
	super();

	this.mapList = new ArrayList<Map>();
	this.state = SystemState.SELECT;
	this.helpHandler = new HelpHandler();

	this.undoStack = new Stack<MapMemory>();
	this.redoStack = new Stack<MapMemory>();

	this.slider = slider;
	this.slider.addChangeListener(new SliderListener(this));
	this.slider.setVisible(false);
	this.slider.setSnapToTicks(true);
	this.slider.setMajorTickSpacing(1);
	this.slider.setPaintLabels(true);
	this.slider.setPaintTicks(true);
	this.setBorder(BorderFactory.createLoweredBevelBorder());

	this.startingDimension = new Dimension(540, 500);
    }

    public void addIncompleteObject(MapObject object)
    {
	this.lastObjectCreated = object;
    }

    @Override
    public void paintComponent(Graphics g)
    {
	super.paintComponent(g);

	if (this.displayMap != null)
	{
	    Dimension mapDimension = new Dimension(this.displayMap.getEndXCoord() - this.displayMap.getStartXCoord(), this.displayMap.getEndYCoord() - this.displayMap.getStartYCoord());
	    int newWidth;
	    int newHeight;
	    if (mapDimension.width >= this.getWidth())
	    {
		newWidth = mapDimension.width;
	    }
	    else
	    {
		newWidth = (int) this.startingDimension.getWidth();
	    }

	    if (mapDimension.height >= this.getHeight())
	    {
		newHeight = mapDimension.height;
	    }
	    else
	    {
		newHeight = (int) this.startingDimension.getHeight();
	    }

	    this.setPreferredSize(new Dimension(newWidth, newHeight));
	    this.revalidate();

	    this.displayMap.paint(g);
	}
    }

    public void setDisplayMap(Map map)
    {
	this.displayMap = map;
    }

    public Map getMap()
    {
	return this.displayMap;
    }

    public void setSliderMax(int max)
    {
	this.slider.setMaximum(max);
	this.slider.setVisible(this.slider.getMaximum() >= 2);
    }

    public JSlider getSlider()
    {
	return this.slider;
    }

    public void setSystemState(SystemState state)
    {
	this.state = state;
    }

    public boolean isOnMap(int x, int y)
    {
	return x >= this.displayMap.getStartXCoord() && x <= this.displayMap.getEndXCoord() && y >= this.displayMap.getStartYCoord() && y <= this.displayMap.getEndYCoord();
    }

    public void addMapToPanel(Image image)
    {
	this.redoStack.clear();
	Map map = new Map(image, this);
	this.undoStack.push(new AddMapOperation(map, this, this.mapList.size()));

	this.mapList.add(map);
	this.displayMap = map;

	if (this.mapList.size() >= 2)
	    this.slider.setVisible(true);

	this.slider.setMaximum(this.mapList.size() - 1);
	this.slider.setValue(this.mapList.size() - 1);
	this.repaint();
    }

    public void insertMap(Map map, int index)
    {
	this.mapList.add(index, map);

	if (this.mapList.size() >= 2)
	{
	    this.slider.setVisible(true);
	}

	this.slider.setMaximum(this.mapList.size() - 1);
	this.slider.setValue(index);
	this.displayMap = this.mapList.get(index);
	this.repaint();
    }

    public void deleteMap(Map map)
    {
	int mapPos = this.mapList.indexOf(map);
	this.mapList.remove(map);
	if (this.mapList.size() == 0)
	{
	    this.displayMap = null;
	    this.slider.setValue(0);
	}
	else
	{
	    if (this.displayMap == map)
	    {
		this.displayMap = (mapPos == this.mapList.size()) ? this.mapList.get(mapPos - 1) : this.mapList.get(mapPos);
		this.slider.setValue(this.mapList.indexOf(this.displayMap));
	    }
	}

	if (this.mapList.size() <= 1)
	{
	    this.slider.setVisible(false);
	}
	else
	{
	    this.slider.setMaximum(this.mapList.size() - 1);
	}
	this.repaint();
    }

    public void deleteMapObject(Map map, MapObject object)
    {
	map.removeMapObject(object);
    }

    public void deleteCurrentMap()
    {
	this.redoStack.clear();
	this.undoStack.push(new DeleteMapOperation(this.displayMap, this, this.mapList.indexOf(this.displayMap)));
	this.mapList.remove(this.displayMap);

	switch (this.mapList.size())
	{
	case 0:
	    this.displayMap = null;
	    this.slider.setVisible(false);
	    break;
	case 1:
	    this.displayMap = this.mapList.get(0);
	    this.slider.setVisible(false);
	    this.slider.setValue(1);
	    break;
	default:
	    this.displayMap = this.mapList.get(0);
	    this.slider.setMaximum(this.mapList.size() - 1);
	    this.slider.setValue(1);
	    break;
	}

	this.repaint();
    }

    public void undo()
    {
	if (this.undoStack.isEmpty())
	{
	    return;
	}
	this.state = SystemState.SELECT;

	MapMemory operation = this.undoStack.pop();
	operation.undo();
	this.redoStack.push(operation);

	this.repaint();
	this.revalidate();
    }

    public void redo()
    {
	if (this.redoStack.isEmpty())
	{
	    return;
	}
	MapMemory operation = this.redoStack.pop();
	operation.redo();
	this.undoStack.push(operation);

	this.repaint();
	this.revalidate();
    }

    public Stack<MapMemory> getUndoStack()
    {
	return this.undoStack;
    }

    public Stack<MapMemory> getRedoStack()
    {
	return this.redoStack;
    }

    public ArrayList<Map> getMapList()
    {
	return this.mapList;
    }

    public void setMapList(ArrayList<Map> maps)
    {
	this.mapList = maps;
    }

    public HelpHandler getHelpHandler()
    {
	return this.helpHandler;
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {    }

    @Override
    public void mouseEntered(MouseEvent e)
    {    }

    @Override
    public void mouseExited(MouseEvent e)
    {    }

    @Override
    public void mousePressed(MouseEvent e)
    {
	if (this.displayMap == null)
	{
	    return;
	}

	if (e.getModifiers() == this.RIGHT_MOUSE_MODIFIER && this.state == SystemState.ADDREGIONPOINTS)
	{
	    if (((Region) this.lastObjectCreated).getNumPoints() < 3)
	    {
		JOptionPane.showMessageDialog(null, "A Region must have three or more points", "Region Error", JOptionPane.WARNING_MESSAGE);
		return;
	    }
	    this.state = SystemState.SELECT;
	    this.repaint();
	}

	switch (this.state)
	{
	case ADDOBJECT:
	    this.redoStack.clear();
	    if (!isOnMap(e.getX(), e.getY()))
		return;

	    this.lastObjectCreated.setLoc(e.getX(), e.getY());
	    this.displayMap.addMapObject(this.lastObjectCreated);

	    this.redoStack.clear();
	    this.undoStack.push(new AddMapObjectOperation(this.lastObjectCreated, this.displayMap.getMapObjects().indexOf(this.lastObjectCreated), this));

	    if (this.lastObjectCreated instanceof Pin)
	    {
		this.state = SystemState.SELECT;
	    }
	    else
	    {
		this.state = SystemState.ADDREGIONPOINTS;
	    }
	    
	    repaint();
	    break;

	case ADDREGIONPOINTS:
	    this.redoStack.clear();
	    if (!isOnMap(e.getX(), e.getY()))
		return;

	    ((Region) this.lastObjectCreated).addPoint(e.getX(), e.getY());
	    repaint();
	    break;

	case EDITMAPOBJECT:
	    this.redoStack.clear();

	    MapObject object;
	    
	    for (int i = 0; i < this.displayMap.getMapObjects().size(); i++)
	    {
		object = this.displayMap.getMapObjects().get(i);
		
		if (object.isContained(e.getX(), e.getY()) && object.visibility)
		{
		    MapObject oldObject = object.copy();
		    this.undoStack.push(new EditMapObjectOperation(oldObject, object, this));
		    this.state = SystemState.SELECT;
		    object.displayCreatePrompt(true);
		}
	    }
	    repaint();
	    break;

	case SELECT:
	    /*
	     * the selection check is done backwards because we want to favor the newest objects over the older ones the newest objects will be at the rear of the array
	     */
	    for (int i = this.displayMap.getMapObjects().size() - 1; i >= 0; i--)
	    {
		if (this.displayMap.getMapObjects().get(i).isContained(e.getX(), e.getY()) & this.displayMap.getMapObjects().get(i).visibility)
		{
		    this.displayMap.getMapObjects().get(i).displayViewPrompt();
		    return;
		}
	    }

	    this.repaint();
	}

    }

    @Override
    public void mouseReleased(MouseEvent e)
    {    }

    @Override
    public void mouseDragged(MouseEvent arg0)
    {    }

    @Override
    public void mouseMoved(MouseEvent e)
    {    }

    private class SliderListener implements ChangeListener, Serializable
    {
	private static final long serialVersionUID = -485832356051623791L;
	
	MapPanel panel;

	public SliderListener(MapPanel relatedPanel)
	{
	    this.panel = relatedPanel;
	}

	@Override
	public void stateChanged(ChangeEvent e)
	{
	    if (this.panel.mapList.isEmpty())
	    {
		// if there is no map, we don't care what the slider does
		return;
	    }
	    MapPanel.this.setDisplayMap(this.panel.mapList.get(((JSlider) e.getSource()).getValue()));
	    MapPanel.this.repaint();
	}

    }

    public void createHelpHandler()
    {
	this.helpHandler = new HelpHandler();

    }
}
